﻿using Microsoft.Kinect;
using Microsoft.Speech.AudioFormat;
using Microsoft.Speech.Recognition;
using System;
using System.Collections.Generic;
using System.Linq;

namespace KinectController
{
    internal class KinectAudio : IDisposable
    {
        #region Private members
        /// <summary>
        /// Kinect sensor
        /// </summary>
        private readonly KinectSensor _sensor;

        /// <summary>
        /// List of active commands
        /// </summary>
        private IList<AudioCommand> _commands;

        /// <summary>
        /// Last recognized command
        /// </summary>
        private AudioCommand _lastCommand;

        /// <summary>
        /// Speech recognition engine using audio data from Kinect.
        /// </summary>
        private SpeechRecognitionEngine _speechEngine;

        #endregion

        #region Public Properties

        public AudioCommand LastCommand
        {
            get { return _lastCommand; }
        }

        #endregion

        #region Events

        /// <summary>
        /// New command received
        /// </summary>
        public event EventHandler<AudioCommandEventArgs> _newCommand;

        #endregion

        #region ctor

        public KinectAudio(KinectSensor sensor, IList<AudioCommand> commands = null)
        {
            _sensor = sensor;
            _commands = new List<AudioCommand>();

            if (_sensor != null)
            {
                RecognizerInfo ri = GetKinectRecognizer();

                if (null != ri)
                {
                    // recognitionSpans = new List<Span> { forwardSpan, backSpan, rightSpan, leftSpan };

                    this._speechEngine = new SpeechRecognitionEngine(ri.Id);


                    // Use this code to create grammar programmatically rather than from
                    // a grammar file.

                    var choices = new Choices();
                    if (commands != null)
                    {
                        foreach (var command in commands)
                        {
                            _commands.Add(command);
                            choices.Add(new SemanticResultValue(command.CommandPhrase, command.CommandName));
                        }
                    }

                    var gb = new GrammarBuilder { Culture = ri.Culture };
                    gb.Append(choices);
                    var g = new Grammar(gb);

                    _speechEngine.LoadGrammar(g);

                    _speechEngine.SpeechRecognized += SpeechRecognized;
                    _speechEngine.SpeechRecognitionRejected += SpeechRejected;

                    _speechEngine.SetInputToAudioStream(this._sensor.AudioSource.Start(),
                        new SpeechAudioFormatInfo(EncodingFormat.Pcm, 16000, 16, 1, 32000, 2, null));
                    _speechEngine.RecognizeAsync(RecognizeMode.Multiple);
                }
            }

            _lastCommand = null;
        }

        #endregion

        #region Public Methods

        public void AddCommand(AudioCommand newCommand)
        {
            _commands.Add(newCommand);
            // TODO : Add updating grammar in speech engine
        }

        #endregion

        #region Private methods

        /// <summary>
        /// Gets the metadata for the speech recognizer (acoustic model) most suitable to
        /// process audio from Kinect device.
        /// </summary>
        /// <returns>
        /// RecognizerInfo if found, <code>null</code> otherwise.
        /// </returns>
        private static RecognizerInfo GetKinectRecognizer()
        {
            var recList = SpeechRecognitionEngine.InstalledRecognizers();
            foreach (RecognizerInfo recognizer in SpeechRecognitionEngine.InstalledRecognizers())
            {
                string value;
                recognizer.AdditionalInfo.TryGetValue("Kinect", out value);
                if ("True".Equals(value, StringComparison.OrdinalIgnoreCase) && "en-US".Equals(recognizer.Culture.Name, StringComparison.OrdinalIgnoreCase))
                {
                    return recognizer;
                }
            }

            return null;
        }

        /// <summary>
        /// Handler for recognized speech events.
        /// </summary>
        /// <param name="sender">object sending the event.</param>
        /// <param name="e">event arguments.</param>
        private void SpeechRecognized(object sender, SpeechRecognizedEventArgs e)
        {
            // Speech utterance confidence below which we treat speech as if it hadn't been heard
            const double ConfidenceThreshold = 0.3;
    
            if (e.Result.Confidence >= ConfidenceThreshold)
            {
                _lastCommand = _commands.Single(x => x.CommandName == e.Result.Semantics.Value.ToString());
                var audioCommandEventArg = new AudioCommandEventArgs();
                audioCommandEventArg.AudioCommand = _lastCommand;
                audioCommandEventArg.ConfidenceThreshold = ConfidenceThreshold;
                audioCommandEventArg.BeamAngle = (int)_sensor.AudioSource.BeamAngle;
                audioCommandEventArg.SourceAngle = (int)_sensor.AudioSource.SoundSourceAngle;
                OnNewCommand(audioCommandEventArg);
            }
        }

        /// <summary>
        /// Handler for rejected speech events.
        /// </summary>
        /// <param name="sender">object sending the event.</param>
        /// <param name="e">event arguments.</param>
        private void SpeechRejected(object sender, SpeechRecognitionRejectedEventArgs e)
        {
        }

        public void Dispose()
        {
            if (this._speechEngine != null)
            {
                this._speechEngine.SpeechRecognized -= SpeechRecognized;
                this._speechEngine.SpeechRecognitionRejected -= SpeechRejected;
                this._speechEngine.RecognizeAsyncStop();
                this._speechEngine.Dispose();
            }
        }

        protected virtual void OnNewCommand(AudioCommandEventArgs e)
        {
            EventHandler<AudioCommandEventArgs> handler = _newCommand;
            if (handler != null)
            {
                handler(this, e);
            }
        }

        #endregion
    }
}
